<?php

/**
 * Class JKSocket
 *
 * @method triggerListen($argument=[])
 * @method triggerConnect($argument=[])
 * @method triggerClose($argument=[])
 * @method triggerBeforeConnect($argument=[])
 * @method triggerRecv($argument=[])
 * @method triggerRecvStep($argument=[])
 * @method triggerBeforeRecvStep($argument=[])
 *
 * @method onListen($callbale)
 * @method onConnect($callbale)
 * @method onClose($callbale)
 * @method onRecv($callbale)
 * @method onRecvStep($callbale)
 * @method onBeforeRecvStep($callbale)
 *
 * @method offListen($callbale)
 * @method offConnect($callbale)
 * @method offClose($callbale)
 * @method offRecv($callbale)
 * @method offRecvStep($callbale)
 * @method offBeforeRecvStep($callbale)
 */
class JKSocket
{
    protected $_port;
    protected $_address;
    protected $_sock;
    protected $_socketClients;
    protected $_queueSockets;
    protected $_callables=[];
    protected $_ok=false;
    protected $_errorCode;
    protected $_errorMsg;
    protected $_values=[];
    protected $_config;
    protected $_argumentClass = 'JKSocketArgument';
    
    public $buffer=null;
    
    const RECV_MODE_BIN = PHP_BINARY_READ;
    const RECV_MODE_NOR = PHP_NORMAL_READ;
    
    const EXP_NOT_A_CALLABLE = "The argument is not a callable. calling method '%s'";

    public function __construct($port=null, $addr='127.0.0.1')
    {
        $this->setPort($port);
        $this->setAddr($addr);
    }
    
    public function config($key, $value=null) {
        if(func_num_args() > 1) {
            $old = $this->_config[$key];
            $this->_config[$key] = $value;
            return $old;
        } else {
            return $this->_config[$key];
        }
    }
    
    public function set($key, $value=null) {
        $this->_values[$key] = $value;
        return $this;
    }
    
    public function get($key, $default=null) {
        return isset($this->_values[$key]) ? $this->_values[$key] : $default;
    }
    
    public function enableKey($key) {
        return $this->set($key, true);
    }
    
    public function remove($key) {
        unset($this->_values[$key]);
        return $this;
    }

    public function disableKey($key)
    {
        return $this->set($key, false);
    }
    
    public function setKeyArrayValue($key, $index, $value)
    {
        $args = func_get_args();
        $k = array_shift($args);
        $v = array_pop($args);
        $lastIndex = array_pop($args);
        
        if(!isset($this->_values[$k]) || !is_array($this->_values[$k])) {
            $this->_values[$k] = [];
        }
        
        $value = &$this->_values[$k];

        foreach ($args as $i) {
            if(!isset($value[$i]) || !is_array($value[$i])) {
                $value[$i] = [];
            }

            $value = &$value[$i];
        }
        
        $value[$lastIndex] = $v;
        return $this;
    }
    
    public function getKeyArrayValue($key, $index) {
        $args = func_get_args();
        $k = array_shift($args);
        $value = $this->_values[$k];
        
        foreach ($args as $i) {
            if(!isset($value[$i])) {
                return null;
            } else {
                $value = $value[$i];
            }
        }
        
        return $value;
    }
    
    public function removeKeyArrayValue($key, $index) {
        $args = func_get_args();
        $k = array_shift($args);
        $lastIndex = array_pop($args);

        if(!isset($this->_values[$k]) || !is_array($this->_values[$k])) {
            return $this;
        }

        $value = &$this->_values[$k];

        foreach ($args as $i) {
            if(!isset($value[$i]) || !is_array($value[$i])) {
                return $this;
            } else {
                $value = &$value[$i];
            }
        }

        unset($value[$lastIndex]);
        return $this;
    }
    
    public function setPort($port)
    {
        $this->_port = $port;
    }
    
    public function setAddr($addr) {
        $this->_address = $addr;
    }
    
    public function getAddr() {
        return $this->_address;
    }
    
    public function getPort() {
        return $this->_port;
    }

    public function fail()
    {
        return !$this->_ok;
    }

    public function off($etype, $callback) {
        if(!isset($this->_callables[$etype])) {
            return $this;
        }

        foreach ($this->_callables[$etype] as $i => $callable) {
            if($callable === $callback) {
                unset($this->_callables[$etype][$i]);
            }
        }

        return $this;
    }
    
    public function __call($function, $argument) {
        $function = strtolower($function);
        $prefix = substr($function, 0, 2);
        
        if($prefix == 'on') {
            if(is_callable($argument[0])) {
                $eventName = substr($function, 2);
                $this->on($eventName, $argument[0]);
            } else {
                $this->_exception(self::EXP_NOT_A_CALLABLE, $function);
            }
        }

        $prefix = substr($function, 0, 7);
        if($prefix == 'trigger') {
            $eventName = substr($function, 7);
            $this->trigger($eventName, $argument);
        }

        $prefix = substr($function, 0, 3);
        if($prefix == 'off') {
            $eventName = substr($function, 7);
            $this->off($eventName, $argument[0]);
        }
        
        return $this;
    }
    
    public function listen()
    {
        if($this->_sock) {
            return false;
        }
        
        $this->_sock = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
        
        if(!$this->_sock) {
            return $this->_collectError();
        }
        
        socket_set_option($this->_sock, SOL_SOCKET, SO_REUSEADDR, 1);
        if(!($this->_ok = socket_bind($this->_sock, $this->_address, $this->_port))) {
            return $this->_collectError();
        }

        if(!($this->_ok = socket_listen($this->_sock))) {
            return $this->_collectError();
        }
        
        $this->triggerListen($this->_createArgument());
        return $this->_processRecv();
    }
    
    public function setSocket($socket) {
        $this->_sock = $socket;
        return $this;
    }
    
    public function getIndex() {
        return (int)$this->getSocket();
    }
    
    public function getAllClients() {
        return $this->_socketClients;
    }
    
    protected function _processRecv($isClient=false) {
        $queueSockets = &$this->_queueSockets;
        $queueSockets = [$this->_sock];
        
        while(true) {
            $this->triggerBeforeRecvStep($this->_createArgument());
            $socketList = $queueSockets;

            if (socket_select($socketList, $write, $except, 0, 30) < 1) {
                $this->triggerRecvStep();
                continue;
            }
            
            /** @var JKSocketArgument $argm */
            $argm = $this->_createArgument();
            
            //new connection
            if (!$isClient && in_array($this->_sock, $socketList)) {
                $queueSockets[] = $newSocket = socket_accept($this->_sock);
                socket_getpeername($newSocket, $ip);
                $className = get_class($this);
                /** @var JKSocket $clientObj */
                $clientObj = new $className(null, $ip);
                $clientObj->setSocket($newSocket);
                $this->_socketClients[(int)$newSocket] = $clientObj;
                $argm->setClient($clientObj);
                $this->triggerConnect($argm);

                $key = array_search($this->_sock, $socketList);
                unset($socketList[$key]);
            }
            
            foreach ($socketList as $socket) {
                $flag = $this->config('recv_mode') ? $this->config('recv_mode') : self::RECV_MODE_NOR;
                $data = @socket_read($socket, 1024*10*1024, $flag);
                
                while(true) {
                    $tmpData = @socket_read($socket, 1024*10*1024, $flag);
                    
                    if(!$tmpData) {
                        break;
                    }
                    
                    $data .= $tmpData;
                }
                
                $index = (int)$socket;

                /** @var JKSocket $client */
                if($isClient) {
                    $client = $this;
                } else {
                    $client = $this->_socketClients[$index];
                }
                
                // check if the client is disconnected
                if ($data === false || $data === '') {
                    $this->closeClient($client);
                    
                    if($isClient) {
                        return $this;
                    } else {
                        continue;
                    }
                }
                
                if($data === '') {
                    continue;
                }

                // $data = trim($data);
                
                if($data !== '') {
                    $argm->setClient($client)->setData($data);
                    $this->triggerRecv($argm);
                }
            }
            
            $this->triggerRecvStep($argm);
        }

        return $this;
    }
    
    public function closeClient(self $client) {
        $key = array_search($client->getSocket(), $this->_queueSockets);
        /** @var JKSocketArgument $argm */
        $argm = $this->_createArgument();
        $this->triggerClose($argm->setClient($client));
        $client->close();
        unset($this->_queueSockets[$key]);
        unset($this->_socketClients[$client->getIndex()]);
        unset($client);
        return $this;
    }
    
    protected function _createArgument() {
        $argmClass = $this->_argumentClass;
        return new $argmClass();
    }
    
    public function connect()
    {
        if($this->_sock) {
            return false;
        }
    
        /** @var JKSocketArgument $argm */
        $argm = $this->_createArgument();
        $this->triggerBeforeConnect($argm);
        $this->_sock = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
        $this->_ok = @socket_connect($this->_sock, $this->_address, $this->_port);

        if($this->fail()) {
            return $this->_collectError();
        }

        $this->triggerConnect($argm->setClient($this));
        return $this->_processRecv(true);
    }
    
    public function send($data) {
        socket_write($this->_sock, $data);
        return $this->_collectError();
    }
    
    public function on($eventName, callable $callback) {
        $eventName = strtolower(trim($eventName));
        
        if(!isset($this->_callables[$eventName])) {
            $this->_callables[$eventName] = [];
        }
        
        $this->_callables[$eventName] []= $callback;
        return $this;
    }
    
    protected function _exception($message, $val1=null, $valn=null) {
        throw new \Exception(call_user_func_array('sprintf', func_get_args()));
    }

    public function trigger($eventName, $args=[]) {
        $eventName = strtolower(trim($eventName));
        
        if(!isset($this->_callables[$eventName])) {
            return $this;
        }

        $arg = isset($args[0]) ? $args[0] : $this->_createArgument();
        
        if(!$arg instanceof JKSocketArgument) {
            throw new Exception('Argument type error when trigger ' . $eventName);
        }
    
        $arg->setEventName($eventName);
        
        foreach((array)$this->_callables[$eventName] as $callable) {
            
            if(!$arg->isPropagation()) {
                return $this;
            }
    
            $arg->setCallable($callable);
            $callable($this, $arg);
        }
        
        return $this;
    }
    
    protected function _collectError()
    {
        $this->_errorCode = socket_last_error($this->_sock);
        $this->_errorMsg = socket_strerror($this->_errorCode);
        return $this;
    }
    
    public function getLastErrorCode() {
        return $this->_errorCode;
    }
    
    public function getLastError() {
        return $this->_errorMsg;
    }
    
    public function close()
    {
        if($this->_sock) {
            socket_close($this->getSocket());
            $this->_sock = null;
        }
        
    }
    
    public function getSocket() {
        return $this->_sock;
    }
}